package com.natalia.file.service.impl.local;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.PrimitiveArrayUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.natalia.file.config.FileProperties;
import com.natalia.file.constants.FileConstants;
import com.natalia.file.entity.*;
import com.natalia.file.enums.FileEnum;
import com.natalia.file.enums.StorageEnum;
import com.natalia.file.exception.FileException;
import com.natalia.file.mapper.UploadMapper;
import com.natalia.file.mapper.UploadPartMapper;
import com.natalia.file.service.FileService;
import com.natalia.file.struct.UploadStructBasic;
import com.natalia.file.util.FileInfo;
import com.natalia.file.util.MimeType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.WebRequest;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Author yang
 **/
@Slf4j
public class LocalServiceImpl implements FileService {

    @Resource
    UploadMapper uploadMapper;

    @Resource
    FileProperties localProperties;

    @Resource
    UploadPartMapper uploadPartMapper;

    @Override
    public UploadResult upload(UploadInfo uploadInfo) {
        try {
            String filePath = generatePath(uploadInfo);
            Path path = Paths.get(filePath);
            Files.createDirectories(path);
            String fileName = UUID.randomUUID().toString();
            if (uploadInfo.isKeepExt()) {
                fileName += FileConstants.EXTENSION_SEPARATOR.concat(FilenameUtils.getExtension(uploadInfo.getFileName()));
            }
            String fullFileName = filePath.concat(fileName);
            Path route = Paths.get(fullFileName);
            Files.write(route, uploadInfo.getContent());
            //处理返回信息
            UploadResult uploadResult = UploadStructBasic.INSTANCES.toUploadResult(uploadInfo);
            String objectName = FileInfo.getFilename(uploadInfo.getPathPrefix(), uploadInfo.getFileName());
            String contentType = MimeType.getMimeType(FilenameUtils.getExtension(objectName));
            uploadResult.setFileExt(FilenameUtils.getExtension(objectName));
            uploadResult.setMimeType(contentType);
            uploadResult.setOriginUrl(route.toString());
            return uploadResult;
        } catch (IOException e) {
            log.error("upload file error:{}", ExceptionUtils.getStackTrace(e));
            return null;
        }
    }

    /**
     *  下载文件
     * @param fileId
     * @param request
     * @return
     */
    @Override
    public ResponseEntity<InputStreamSource> download(String fileId, WebRequest request) {
        UploadInfo uploadInfo = uploadMapper.selectOne(
                Wrappers.lambdaQuery(UploadInfo.class).eq(UploadInfo::getFileId, fileId)
        );
        Assert.isTrue(ObjectUtil.isNotEmpty(uploadInfo), "file does not exist.");
        Path path = Paths.get(uploadInfo.getFilePath());
        byte[] bytes;
        try {
            bytes = Files.readAllBytes(path);
        } catch (IOException e) {
            throw new FileException("error reading file", e);
        }
        long lastModified = uploadInfo.getUpdatedTime().getTime();
        String eTag = String.valueOf(lastModified + uploadInfo.getFileSize());
        if (request.checkNotModified(eTag, lastModified)) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }
        if (PrimitiveArrayUtil.isNotEmpty(bytes)) {
            return ResponseEntity.ok()
                    .eTag(eTag)
                    .lastModified(lastModified)
                    .cacheControl(CacheControl.maxAge(60, TimeUnit.SECONDS))
                    .contentType(MediaType.parseMediaType(
                            MimeType.TYPES.getOrDefault(uploadInfo.getFileExt(), MediaType.APPLICATION_OCTET_STREAM_VALUE)))
                    .body(new ByteArrayResource(bytes));
        } else {
            return ResponseEntity.notFound().build();
        }
    }

    @Override
    public MultipartUploadInfo createMultipartInit(MultipartInit multipartInit) {
        UploadInfo retrievedUploadInfo = uploadMapper.selectOne(
                Wrappers.lambdaQuery(UploadInfo.class).eq(UploadInfo::getFileMd5, multipartInit.getFileMd5()));
        if (ObjectUtil.isEmpty(retrievedUploadInfo)) {
            MultipartUploadInfo uploadInfo = new MultipartUploadInfo();
            uploadInfo.setUploadId(IdUtil.simpleUUID());
            uploadInfo.setPartNum(multipartInit.getPartNum());
            uploadInfo.setFileName(multipartInit.getFileName());
            UploadInfo upload = this.saveUpload(multipartInit, uploadInfo);
            return MultipartUploadInfo.builder()
                    .uploadId(upload.getUploadId())
                    .partNum(upload.getPartNum())
                    .fileName(upload.getFileSourceName())
                    .build();
        }
       return MultipartUploadInfo.builder()
                .uploadId(retrievedUploadInfo.getUploadId())
                .partNum(multipartInit.getPartNum())
                .fileName(retrievedUploadInfo.getFileSourceName())
                .build();
    }

    @Override
    public MultipartUploadInfo uploadPart(UploadPartInfo uploadPartInfo) {
        LambdaQueryWrapper<UploadPartInfo> queryWrapper = Wrappers.lambdaQuery(UploadPartInfo.class)
                .eq(UploadPartInfo::getUploadId, uploadPartInfo.getUploadId())
                .eq(UploadPartInfo::getPartNumber, uploadPartInfo.getPartNumber());
        UploadPartInfo existingPartInfo = uploadPartMapper.selectOne(queryWrapper);
        if (existingPartInfo != null) {
            return MultipartUploadInfo.builder()
                    .uploadId(uploadPartInfo.getUploadId())
                    .partNum(uploadPartInfo.getPartNumber())
                    .fileName(existingPartInfo.getFileSourceName())
                    .build();
        }
        String partFileFullPath = saveFile(uploadPartInfo);
        return partInfoSave(uploadPartInfo, partFileFullPath);
    }


    @Override
    public List<ShardUploadInfo> listParts(MultipartUploadCreate uploadCreate) {
        List<UploadPartInfo> partInfos = uploadPartMapper.selectList(Wrappers.lambdaQuery(UploadPartInfo.class)
                .eq(UploadPartInfo::getUploadId, uploadCreate.getUploadId())
                .eq(UploadPartInfo::getFileSourceName, uploadCreate.getObjectName())
                .orderByAsc(UploadPartInfo::getPartNumber));
        return UploadStructBasic.INSTANCES.uploadPartToShardUploadInfos(partInfos);
    }

    @Override
    public CompleteUploadInfo completeMultipart(CompleteMultipart request) {
        UploadInfo retrievedUploadInfo = uploadMapper.selectOne(
                Wrappers.lambdaQuery(UploadInfo.class)
                        .eq(UploadInfo::getUploadId, request.getUploadId())
                        .eq(UploadInfo::getFileSourceName, request.getFileName()));
        if (ObjectUtil.isNotEmpty(retrievedUploadInfo) &&
                FileEnum.YES.getValue()==retrievedUploadInfo.getFileStatus()) {
            return CompleteUploadInfo.builder()
                    .uploadId(retrievedUploadInfo.getUploadId())
                    .uploadName(retrievedUploadInfo.getFileSourceName())
                    .url(retrievedUploadInfo.getFilePath())
                    .size(retrievedUploadInfo.getFileSize())
                    .build();
        }
        List<UploadPartInfo> partInfos = this.fileVerification(request);
        String partFileFullPath = Paths.get(localProperties.getRootPath(),
                request.getUploadId(), retrievedUploadInfo.getFileSourceName()).toString();
        File file = FileInfo.createFileNotExists(new File(partFileFullPath));
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = FileUtils.openOutputStream(file, true);
            for (UploadPartInfo part : partInfos) {
                File partFile = new File(part.getFileFullPath());
                FileInputStream partFileInputStream = null;
                try {
                    partFileInputStream = FileUtils.openInputStream(partFile);
                    IOUtils.copyLarge(partFileInputStream, fileOutputStream);
                } finally {
                    IOUtils.closeQuietly(partFileInputStream);
                }
                partFile.delete();
            }
        }  catch (IOException e) {
           throw new FileException("failed to process sharding task", e);
       }finally {
            IOUtils.closeQuietly(fileOutputStream);
        }
        this.fileVerificationToDb(retrievedUploadInfo,file);
        return CompleteUploadInfo.builder()
                .uploadId(retrievedUploadInfo.getUploadId())
                .uploadName(retrievedUploadInfo.getFileSourceName())
                .url(localProperties.getFilePath().concat(retrievedUploadInfo.getFileId()))
                .size(retrievedUploadInfo.getFileSize())
                .build();
    }

    /**
     *  查询分片信息
     * @param completeMultipart
     * @return
     */
    private List<UploadPartInfo> fileVerification(CompleteMultipart completeMultipart){
        UploadPartInfo uploadPartInfo = new UploadPartInfo();
        uploadPartInfo.setUploadId(completeMultipart.getUploadId());
        List<UploadPartInfo> partInfos = uploadPartMapper.selectList(Wrappers.lambdaQuery(UploadPartInfo.class)
                .eq(UploadPartInfo::getUploadId, completeMultipart.getUploadId())
                .orderByAsc(UploadPartInfo::getPartNumber));
        Assert.notEmpty(partInfos, "Sharding task does not exist");
        Assert.isTrue(partInfos.size() == completeMultipart.getChunks(), "task does not have the correct number of parts");
        return partInfos;
    }

    /**
     *  记录到数据库
     * @param multipartInit
     * @param uploadInfo
     */
    private UploadInfo saveUpload(MultipartInit multipartInit, MultipartUploadInfo uploadInfo){
        UploadInfo multipart = UploadStructBasic.INSTANCES.multipartToUploadInfo(uploadInfo);
        String fileSourceName =IdUtil.fastSimpleUUID()+FileConstants.EXTENSION_SEPARATOR+ FilenameUtils.getExtension(multipartInit.getFileName());
        String contentType = MimeType.getMimeType(FilenameUtils.getExtension(multipartInit.getFileName()));
        multipart.setFileId(IdUtil.getSnowflakeNextIdStr());
        multipart.setFileMd5(multipartInit.getFileMd5());
        multipart.setStorageType(StorageEnum.LOCAL.getValue());
        multipart.setFileExt(FilenameUtils.getExtension(multipartInit.getFileName()));
        multipart.setMimeType(contentType);
        multipart.setFileStatus(FileEnum.NO.getValue());
        multipart.setFileSourceName(fileSourceName);
        multipart.setCreatedTime(new Date());
        multipart.setUpdatedTime(new Date());
        uploadMapper.insert(multipart);
        return multipart;
    }

    /**
     * 验证md5并写入文件地址
     * @param retrievedUploadInfo
     * @param file
     */
    private void fileVerificationToDb(UploadInfo retrievedUploadInfo,File file){
        Assert.isTrue(StringUtils.isBlank(retrievedUploadInfo.getFileMd5())
                || retrievedUploadInfo.getFileMd5().equals(SecureUtil.md5(file)), "file md5 does not match");
        retrievedUploadInfo.setFilePath(file.getAbsolutePath());
        retrievedUploadInfo.setFileSize(file.length());
        retrievedUploadInfo.setFileStatus(FileEnum.YES.getValue());
        retrievedUploadInfo.setFileUrl(localProperties.getFilePath().concat(retrievedUploadInfo.getFileId()));
        uploadMapper.updateById(retrievedUploadInfo);
    }

    /**
     * 保存文件
     *
     * @param uploadPartInfo
     * @return
     */
    private String saveFile(UploadPartInfo uploadPartInfo) {
        try {
            String partFileFullPath = Paths.get(localProperties.getRootPath(),
                    uploadPartInfo.getUploadId(),
                    String.valueOf(uploadPartInfo.getPartNumber())).toString();
            File file = new File(partFileFullPath);
            FileInfo.createFileNotExists(file);
            Path path = Paths.get(file.getAbsolutePath());
            Files.copy(uploadPartInfo.getFile().getInputStream(), path, StandardCopyOption.REPLACE_EXISTING);
            return partFileFullPath;
        } catch (IOException e) {
            throw new FileException("Failed to transfer file", e);
        }
    }

    /**
     * 将分片文件信息写入数据库
     *
     * @param uploadPartInfo
     * @param partFileFullPath
     * @return
     */
    private MultipartUploadInfo partInfoSave(UploadPartInfo uploadPartInfo, String partFileFullPath) {
        UploadInfo uploadInfo = uploadMapper.selectOne(
                Wrappers.lambdaQuery(UploadInfo.class).eq(UploadInfo::getUploadId,uploadPartInfo.getUploadId()));
        UploadPartInfo newPartInfo = new UploadPartInfo();
        newPartInfo.setPartId(IdUtil.getSnowflakeNextIdStr());
        newPartInfo.setUploadId(uploadPartInfo.getUploadId());
        newPartInfo.setPartNumber(uploadPartInfo.getPartNumber());
        newPartInfo.setFileFullPath(partFileFullPath);
        newPartInfo.setFileSourceName(ObjectUtil.isNotEmpty(uploadInfo) ? uploadInfo.getFileSourceName() : null);
        newPartInfo.setCreatedTime(new Date());
        newPartInfo.setUpdatedTime(new Date());
        uploadPartMapper.insert(newPartInfo);
        return MultipartUploadInfo.builder()
                .uploadId(newPartInfo.getUploadId())
                .partNum(newPartInfo.getPartNumber())
                .fileName(newPartInfo.getFileSourceName())
                .build();
    }

    /**
     * 生成存储路径
     *
     * @return String
     */
    private String generatePath(UploadInfo uploadInfo) {
        StringBuilder buffer = new StringBuilder();
        String rootPath = localProperties.getRootPath();
        Assert.isTrue(!ObjectUtils.isEmpty(rootPath), "the file root path cannot be empty");
        if (!rootPath.startsWith(File.separator)) {
            buffer.append(File.separator);
        }
        buffer.append(rootPath);
        if (!rootPath.endsWith(File.separator)) {
            buffer.append(File.separator);
        }
        if (!ObjectUtils.isEmpty(uploadInfo.getPathPrefix())) {
            buffer.append(uploadInfo.getPathPrefix()).append(File.separator);
        }
        String formattedDate = LocalDate.now().format(DateTimeFormatter.ofPattern(FileConstants.PURE_DATE_PATTERN));
        buffer.append(formattedDate).append(File.separator);
        return buffer.toString();
    }

}
